package tatrus;

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

/**
 * <p>Title: Game </p>
 * <p>Description: CSIS 343 Course Project</p>
 * 
 * This class is responsible for handling the majority of the
 * logic that makes the game run.  It extends the JFrame class,
 * and contains both an instance of the Board class and the 
 * Display class.  
 * 
 * @author Nick Brown
 * @email  nicholas.brown@my.uwrf.edu
 * @author John Wagner 
 * @email  john.wagner@my.uwrf.edu
 * 
 * @date November 28th 2013
 *
 */

public class Game extends JFrame 
{
	private static final long serialVersionUID = 300L;
	public static final int X_OFFSET = 550;
	public static final int Y_OFFSET = 250;
	public static final int FRAME_WIDTH = 466;
	public static final int FRAME_HEIGHT = 538;
	public static final int TIMER_DEFAULT = 400;
	public static final int NUMBER_OF_SHAPES = 7;
	
	private int score, level, currentRow, currentColumn,
				rowsForLevel, timerSpeed;
	
	private boolean isPaused, isGameOver, shapeIsFalling,
					timerIsRunning, isScoreManagerOpen;
	
	private Board board;
	private Display display;
	private Random generator;
	private Shape currentShape, nextShape;
	private Timer gameTimer;
	private HighScoreManager scoreManager;
	
	/**
	 * Creates a new game window.  The game will not start
	 * until the player clicks the "Start New Game" button.
	 */
	public Game()
	{
		display = new Display(this);
		add(display);
		
		board = new Board(this, display);
		add(board);
		
		isScoreManagerOpen = false;
		
		setTitle("Tatrus");
        setLayout(null);
        setBounds(X_OFFSET, Y_OFFSET, FRAME_WIDTH, FRAME_HEIGHT);
        setResizable(false);
		setVisible(true);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		
		/* Adding a KeyListener to the object, which enables the player 
		 * to control the movement of the shapes with the space bar, up,
		 * left, right, and down arrow keys.  A switch statement is used
		 * to determine what action needs to be taken depending on what
		 * key has been pressed.    
		 */
		addKeyListener(new KeyAdapter() 
		{
			public void keyPressed(KeyEvent event) 
			{
				switch(event.getKeyCode()) 
				{
					case KeyEvent.VK_SPACE:
				
					/* Getting the current rotation of the shape, and 
					 * determining the value of the rotation number when
					 * it is rotated 90 degrees counterclockwise.  Once 
					 * the new rotation number is determined, a check is 
					 * performed to make sure that the shape can in fact 
					 * be rotated before actually performing the rotation.
					 */
					int rotationCheck1 = currentShape.getRotationNumber();
						
					rotationCheck1--;
					
					if(rotationCheck1 == -1)
					{
						rotationCheck1 = 3;
					}
					
					if(!isPaused && board.shapeCanMove(currentShape, currentRow,
							   currentColumn, rotationCheck1))
					{
						rotateCounterclockwise();
					}
						
					break;
						
					case KeyEvent.VK_UP:
						
					/* Getting the current rotation of the shape, and 
					 * determining the value of the rotation number when
					 * it is rotated 90 degrees clockwise.  Once the new
					 * rotation number is determined, a check is performed
					 * to make sure that the shape can in fact be rotated
					 * before actually performing the rotation.
					 */
					int rotationCheck2 = currentShape.getRotationNumber();
						
					rotationCheck2++;
					
					if(rotationCheck2 == 4)
					{
						rotationCheck2 = 0;
					}
					
					if(!isPaused && board.shapeCanMove(currentShape, currentRow,
							   currentColumn, rotationCheck2))
					{
						rotateClockwise();
					}
					
					break;
						
					case KeyEvent.VK_LEFT:
						
					/* Checking to see if the shape can be moved one column 
					 * to the left.  If it is possible, then the move is made.
					 */
					if(!isPaused && board.shapeCanMove(currentShape, currentRow,
					   currentColumn - 1, currentShape.getRotationNumber()))
					{
						currentColumn--;
					}
					
					break;
						
					case KeyEvent.VK_RIGHT:
						
					/* Checking to see if the shape can be moved one column 
					 * to the right.  If it is possible, then the move is made.
					 */
					if(!isPaused && board.shapeCanMove(currentShape, currentRow,
					   currentColumn + 1, currentShape.getRotationNumber()))
					{
						currentColumn++;
					}
						
					break;
						
					case KeyEvent.VK_DOWN:
						
					/* The down arrow key causes the shape to fall at high speed.
					 * The while loop continually checks to make sure the shape
					 * can be moved one row down.  When the shape can no longer
					 * move, the loop is exited.
					 */
					if(!isPaused)
					{
						while(board.shapeCanMove(currentShape, currentRow + 1,
							  currentColumn, currentShape.getRotationNumber()))
						{
							currentRow++;
						}
					}
						
					break;	
				}
			}
		});
	}
	
	/**
	 * Begins a new game.  
	 */
	private void startGame() 
	{
		/* Setting all data members to their proper 
		 * values and initializing the game timer.
		 */
		requestFocusInWindow();
		timerIsRunning = true;
		isPaused = false;
		isGameOver = false;
		shapeIsFalling = true;
		generator = new Random();
		currentShape = new Shape(generator.nextInt(NUMBER_OF_SHAPES));
		nextShape = new Shape(generator.nextInt(NUMBER_OF_SHAPES));
		timerSpeed = 250;
		
		gameTimer = new Timer();
	    gameTimer.schedule(new GameTimer(), 0, timerSpeed);
		
	    startNewShape();
	}
	
	/**
	 * Rotates the current Shape in play clockwise.
	 */
	public void rotateClockwise()
	{
		if(currentShape != null)
		{
			int rotation = currentShape.getRotationNumber();
			
			rotation++;
			
			/* Since the rotation values are 0-3, a check  
			 * is performed so that a clockwise rotation at
			 * position 3 will result in a position of 0.  
			 */
			if(rotation == 4)
			{
				rotation = 0;
			}
			
			currentShape.setRotationNumber(rotation);
		}
	}
	
	/**
	 * Rotates the current Shape in play counterclockwise.
	 */
	public void rotateCounterclockwise()
	{
		if(currentShape != null)
		{
			int rotation = currentShape.getRotationNumber();
			
			rotation--;
			
			/* Since the rotation values are 0-3, a check  
			 * is performed so that a counterclockwise rotation
			 * at position 0 will result in a position of 3.  
			 */
			if(rotation == -1)
			{
				rotation = 3;
			}

			currentShape.setRotationNumber(rotation);
		}
	}
	
	/**
	 * Updates the necessary information for 
	 * the game currently being played.
	 */
	public void updateGame() 
	{
		/* Requesting focus every time the 
		 * game is updated, ensuring that key
		 * presses are heard by the KeyListener. 
		 */
		requestFocusInWindow();
		
		// Disposing of the HighScoreManager if it is open
		if(isScoreManagerOpen)    
		{
			scoreManager.dispose();
		}
		
		/* If the current shape has come to rest, the next
		 * shape is moved to the current shape, a new shape
		 * is generated for the next shape, and the new 
		 * current shape is positioned on the board.
		 */
		if(!shapeIsFalling && !isGameOver)
		{
			currentShape = nextShape;
			nextShape = new Shape(generator.nextInt(NUMBER_OF_SHAPES));
			shapeIsFalling = true;
			startNewShape();
		}
		
		/* If the current shape has not come to rest,
		 * an attempt is made to move it down one row.
		 */
		else if(shapeIsFalling && !isGameOver && 
				board.shapeCanMove(currentShape, currentRow + 1,
				currentColumn, currentShape.getRotationNumber()))
		{
			currentRow++;
		}
		
		/* If the shape can't move down to the next row,
		 * it is saved to the board.  All rows of the board
		 * are then checked to see if any rows have been
		 * completed.  The score and level are then updated
		 * according to how many rows were completed.
		 */
		else if(shapeIsFalling && !isGameOver && 
				!board.shapeCanMove(currentShape, currentRow + 1,
				currentColumn, currentShape.getRotationNumber()))
		{
			shapeIsFalling = false;
			board.addShapeToBoard(currentShape, currentRow, currentColumn,
								  currentShape.getRotationNumber());
			
			int rowsCompleted = board.checkAllRows();
			
			updateScore(rowsCompleted);
			updateLevel(rowsCompleted);
		}
	}
	
	/**
	 * Updates the game score based on the number 
	 * of rows cleared and the current level.
	 * 
	 * @param rows The number of rows cleared
	 */
	private void updateScore(int rows)
	{
		if (rows > 0)
		{
			score = score + (int) (Math.pow(2, rows - 1)) * 100 * level;
		}
	}
	
	/**
	 * Updates the level every time 10 rows are cleared.
	 * 
	 * @param rows The number of rows cleared
	 */
	private void updateLevel(int rows)
	{
		rowsForLevel += rows;
		
		if(rowsForLevel >= 10)
		{
			level++;
			rowsForLevel = rowsForLevel % 10;
			timerSpeed -= 20;
		}
	}
	
	/**
	 * Sets the location of every new 
	 * Shape introduced to the board.
	 */
	private void startNewShape()
	{
		currentColumn = currentShape.getColumnIndex();
		
		currentRow = currentShape.getLowerOffset(
					 currentShape.getRotationNumber());
	}
	
	/**
	 * Begins a new game sequence.  
	 */
	public void startNewGame()
	{
		resetGame();
		startGame();
	}
	
	/**
	 * Resets all game information so 
	 * that a new game can be started.
	 */
	private void resetGame()
	{
		level = 1;
		score = 0;
		rowsForLevel = 0;
		board.clearBoard();
		board.setGameOverStatus(false);
	}
	
	/**
	 * Makes calls to repaint both
	 * the display and the board.
	 */
	private void repaintGame() 
	{
        board.repaint();
        display.repaint();
	}
	
	/**
	 * Toggles the pause state of the game.
	 */
	public void togglePause()
	{
		isPaused = !isPaused;
		
		if(isPaused)
		{
			gameTimer.cancel();
			timerIsRunning = false;
		}
		
		else
		{
			timerIsRunning = true;
			gameTimer = new Timer();
			gameTimer.schedule(new GameTimer(), 0, timerSpeed);
		}
	}
	
	/**
	 * Handles all actions that need to be taken 
	 * when game over conditions are triggered.
	 */
	public void runGameOverSequence()
	{	
		boolean highScoreEarned = false;
		
		/* Disposing of the HighScoreManager if it 
		 * happens to be open so that there are no
		 * problems accessing the database.
		 */
		if(isScoreManagerOpen)
		{
			scoreManager.dispose();
		}
		
		/* Creating a new HighScoreManager to
		 * handle the end of game operations.
		 */
		scoreManager = new HighScoreManager(this);
		isScoreManagerOpen = true;
		
		String name;
		
		long numberOfScores = scoreManager.getHighScoreCount();
		
		HighScore highScore;
		
		/* If the number of scores in the database is less
		 * than 10, the player has automatically earned a
		 * high score, and their information is collected.
		 */
		if(numberOfScores < 10)
		{
			highScoreEarned = true;
			
			do 
			{
				name = JOptionPane.showInputDialog(
									"You have earned a top 10 high score!  " +  
									"Please enter your name: ");
			}
			
			while(name == null || name.trim().length() == 0 || 
													name.length() > 15);
			
			highScore = new HighScore(name,score);
			
			/* Displaying the high score list,
			 * including the newly saved high score
			 */
			scoreManager.insertHighScore(highScore);
			scoreManager.setScoreList();
			scoreManager.setFrameVisibility(true);
		}
		
		/* If there are already 10 high scores in the database,
		 * a check is performed to determine if the score earned
		 * by the player is higher than any of the scores in the
		 * database.  If the score is a new high, then the lowest
		 * score is removed from the database and the player's
		 * information is collected and saved.  
		 */
		else if(scoreManager.isHighScore(score))
		{
			highScoreEarned = true;
			
			do 
			{
				name = JOptionPane.showInputDialog(
									"You have earned a top 10 high score!  " +  
									"Please enter your name: ");
			}
			
			while(name == null || name.trim().length() == 0 ||
													name.length() > 15);
			
			highScore = new HighScore(name,score);
			
			/* Displaying the high score list,
			 * including the newly saved high score
			 */
			scoreManager.deleteLowestScore();
			scoreManager.insertHighScore(highScore);
			scoreManager.setScoreList();
			scoreManager.setFrameVisibility(true);
		}
		
		/* If a high score was not earned, the 
		 * HighScoreManager is immediately disposed of.
		 */
		if(!highScoreEarned)
		{
			scoreManager.dispose();  
		}
	}
	
	/**
	 * Returns the current pause state of the game.
	 * 
	 * @return true if the game is paused, otherwise false
	 */
	public boolean isPaused()
	{
		return isPaused;
	}
	
	/**
	 * Returns the score of the current game.
	 * 
	 * @return The score of the current game
	 */
	public int getScore()
	{
        return score;
	}

	/**
	 * Returns the level of the current game.
	 * 
	 * @return The level of the current game
	 */
	public int getLevel() 
	{
		return level;
	}

	/**
	 * Returns the current Shape that is in play
	 * 
	 * @return The current Shape that is in play
	 */
	public Shape getCurrentShape() 
	{
		return currentShape;
	}

	/**
	 * Returns the next Shape that will be given.
	 * 
	 * @return The next Shape that will be given
	 */
	public Shape getNextShape() 
	{
		return nextShape;
	}

	/**
	 * Returns the column of the board where the 
	 * origin of the Shape in play is located.
	 * 
	 * @return The current column of the Shape origin
	 */
	public int getCurrentColumn() 
	{
		return currentColumn;
	}

	/**
	 * Returns the row of the board where the 
	 * origin of the Shape in play is located.
	 * 
	 * @return The current row of the Shape origin
	 */
	public int getCurrentRow() 
	{
		return currentRow;
	}
	
	/**
	 * Used to stop the game timer in the event 
	 * of a pause or game over condition.
	 */
	public void stopTimer()
	{
		gameTimer.cancel();
	}
	
	/**
	 * Returns whether or not a HighScoreManager
	 * object is currently open or not. 
	 * 
	 * @return true if a HighScoreManager object
	 * 		   is in use, otherwise false
	 */
	public boolean isScoreManagerOpen()
	{
		return isScoreManagerOpen;
	}
	
	/**
	 * Sets the state of the 
	 * HighScoreManager to closed.  
	 */
	public void setScoreManagerClosed()
	{
		isScoreManagerOpen = false;
	}
	
	/**
	 * Creates a HighScoreManager object so that database 
	 * operations can be performed for high scores.  
	 */
	public void openScoreManager()
	{
		scoreManager = new HighScoreManager(this);
		scoreManager.getHighScores();
		scoreManager.setScoreList();
		scoreManager.setFrameVisibility(true);
		isScoreManagerOpen = true;
	}
	
	public static void main(String[] args) 
	{
		new Game();
	}
	
	/**
	 * Inner class used in conjunction with  
	 * the Timer to manage the game loop.
	 */
	private class GameTimer extends TimerTask
	{
	    public void run() 
	    {
	    	updateGame();
			repaintGame();

	        if (!timerIsRunning)
	        {
	            gameTimer.cancel();
	        }
	    }
	}
}